/*
 * yocef - Lightweight IoC Event Framework
 *
 * Copyright (c) 2009, Yves Cuillerdier
 * All rights reserved.
 * 
 * This software is licensed under the new BSD License:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of the author nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * @version 1.1.0
 */
package yocef.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import yocef.client.trace.EventEventTrace;

/**
 * Set of all actions registered on an <tt>{Event,Object}</tt> couple.
 * <p>
 * This class represents an association of an <tt>Event</tt> instance with a
 * <b>source</b> <tt>Object</tt>. This association is unique : Two different
 * instances of <tt>EventBind</tt> could not be <tt>equals</tt> (represents the
 * same <tt>{Event,Object}</tt> couple).
 * <p>
 * This class manage a set of <tt>EventBind</tt>. Each <tt>EventBind</tt> is a
 * mapping between an <tt>{Event,Object}</tt> couple and an <tt>EventAction</tt>.
 */
public final class EventBindSet {

	/**
	 * A Map to enforce single instance of a given {Event,Object} couple
	 * <p>
	 * The main key is the source object to facilitate the <tt>off</tt>ing or
	 * cleaning of elements.
	 * <p>
	 * Nota : Need a TreeMap, not only a Map (@see EventBindSet.cleanup)
	 */
	private static Map<Object, TreeMap<Event, EventBindSet>> bindStock = new HashMap<Object, TreeMap<Event, EventBindSet>>();

	/** The Event and Source */
	protected final Event event;
	private final Object source;

	/**
	 * The list of actions registered on the {Event,Object} couple.
	 */
	private final List<EventBind> bindSet = new ArrayList<EventBind>();

	/** Private constructor. Use factory! */
	private EventBindSet(final Event e, final Object s) {
		event = e;
		source = s;
	}

	/**
	 * EventBindSet factory. Ensure that two distinct
	 * <code>{Event,Object}</code> instances are not <code>equals</code>
	 * <p>
	 * Notice: this method is not synchronized. Must we ?
	 * <p>
	 * package visibility!
	 * 
	 * @param event
	 *            the event
	 * @param element
	 *            the source element
	 * @return A EventBindSet for this <tt>{Event,Object}</tt> couple, either an
	 *         old one (if the couple have already be binded), or a fresh one
	 *         (if we never see this couple before).
	 */
	static EventBindSet get(final Event event, final Object element) {
		EventBindSet bind = null;
		TreeMap<Event, EventBindSet> eb = bindStock.get(element);
		if (eb == null) {
			// First time we saw this source element
			eb = new TreeMap<Event, EventBindSet>();
			bindStock.put(element, eb);
		}
		bind = eb.get(event);
		if (bind == null) {
			// First time we saw this event for the element
			bind = new EventBindSet(event, element);
			eb.put(event, bind);
		}
		return bind;
	}

	/**
	 * TODO : Utility ?
	 * 
	 * EventBindSet factory. Ensure that two distinct
	 * <code>{Event,Object}</code> instances are not <code>equals</code>
	 * <p>
	 * Notice: this method is not synchronized. Must we ?
	 * <p>
	 * package visibility!
	 * 
	 * @param eventName
	 *            the event type the source element
	 * @return A EventBind for this <tt>{Event,Object]</tt> couple, either an
	 *         old one (if the couple have already be binded), or a fresh one
	 *         (if we never see this couple before).
	 */
	// static EventBindSet get(final String eventName, final Object element) {
	// return get(Event.get(eventName), element);
	// }

	/**
	 * Add an action to this binding set. A new EventBind is build.
	 * 
	 * @param action
	 *            the action to performed when the event occurs on the element.
	 *            Must not be null
	 * @return this new EventBind for easy chaining (for use with
	 *         <tt>using(...)</tt> and <tt>now()</tt>)
	 * @see EventAction
	 */
	public EventBind perform(final EventAction action) {
		// Must we check that the given action is not already registered for
		// this {Event, Object} ?
		if (action == null) {
			throw new IllegalArgumentException("Action could not be null."); //$NON-NLS-1$
		}
		// Are we in tracing mode ?
		final EventBind bind;
		if ((Event.getTraceAll() || event.getTrace())
				&& event != EventEventTrace.BeforeEventHandling.event()
				&& event != EventEventTrace.EventHandled.event()) {
			// Yes we are (and on interesting events)
			bind = new EventBind(this, new TraceAction(action));
		} else {
			bind = new EventBind(this, action);
		}
		bindSet.add(bind);
		return bind;
	}

	/**
	 * Get the Event associated with this EventBind.
	 * 
	 * @return the event. Not null
	 */
	public Event getEvent() {
		return event;
	}

	/**
	 * Get the source Object associated with this EventBind.
	 * 
	 * @return the source object. Not null
	 */
	public Object getSource() {
		return source;
	}

	/**
	 * Fire all the actions. If one action responds <tt>false</tt> all remaining
	 * actions are skipped.
	 * <p>
	 * package visibility
	 * 
	 * @param params
	 *            the parameters of the event
	 * @return <tt>true</tt> if all individual registered actions has respond
	 *         <tt>true</tt>. Else, return <tt>false</tt>.
	 */
	boolean fire(final EventParams params) {
		// ToArray to avoid ConcurrentModificationException
		for (final EventBind bind : bindSet.toArray(new EventBind[bindSet
				.size()])) {
			if (!bind.fire(params)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Get the EventBindSet hashcode
	 * 
	 * @return the hashcode
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return source.hashCode();
	}

	/**
	 * Test EventBidSet equality. Two "equals" EventBindSet are "identical" (the
	 * same object instance)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(final Object obj) {
		// Two distinct instances cannot be equals
		return this == obj;
	}

	// //////////////////////////////////////////////////////////////
	// Housekeeping
	// ////////////////////////////////////////////////////////////

	/**
	 * Cancel a given action on the element for this Event. From that moment,
	 * the action could no more be fired on the element for this event.
	 * 
	 * @param action
	 *            the action to cancel on the element for this event
	 */
	public void cancel(final EventAction action) {
		for (final EventBind bind : bindSet) {
			if (bind.getAction() == action) {
				bind.free();
				bindSet.remove(bind);
			}
		}
	}

	/**
	 * Objects don't know anything about the events that are set on them. When
	 * an object is no more referred anywhere outside the event framework,
	 * references remaining in this framework prevent the garbage collector to
	 * reclaim the memory.
	 * <p>
	 * This method will remove references that could prevent the object to be
	 * reclaim by the GC.
	 * <p>
	 * package visibility
	 * 
	 * @param element
	 *            the element to "clean"
	 * @see Event#cleanup(Object)
	 */
	static void cleanup(final Object element) {
		final TreeMap<Event, EventBindSet> eb = bindStock.get(element);
		if (eb == null) {
			return;
		}
		// Perform housekeeping for all bindings set
		while (eb.size() > 0) {
			eb.get(eb.firstKey()).unlink();
		}
		// no more bindStock
		bindStock.remove(element);
	}

	/**
	 * Unbind this {element,event} couple. Remove all registered actions.
	 * <p>
	 * package visibility
	 */
	void unlink() {
		for (final EventBind bind : bindSet) {
			bind.free();
		}
		bindSet.clear();
		// Removing itself from the bindStock is enough for the GC to be able to
		// reclaim the memory.
		final Map<Event, EventBindSet> eb = bindStock.get(source);
		if (eb != null) {
			eb.remove(event);
			if (eb.isEmpty()) {
				bindStock.remove(source);
			}
		}
	}

	/**
	 * The EventAction that will fire the two Events for Tracing.
	 * <p>
	 * The Events are:
	 * <ul>
	 * <li>BeforeEventHandling: fired before the call to the
	 * {@link EventAction#handle(Event, Object, EventContext, EventParams)}
	 * method.</li>
	 * <li>EventHandled: fired after the call to the
	 * {@link EventAction#handle(Event, Object, EventContext, EventParams)}
	 * method.</li>
	 * </ul>
	 * These Events are fired:
	 * <ul>
	 * <li>On the Event <b>class</b> if we trace all events</li>
	 * <li>On the Event <b>instance</b> if we just trace this event</li>
	 * </ul>
	 */
	/** ID to trace Event handling */
	protected static int eventCallId = 0;

	private class TraceAction implements EventAction {
		/** trace status at creation time */
		private final boolean traceAll = Event.getTraceAll();
		private final boolean traceMe = event.getTrace();

		/** The true Action */
		final EventAction action;

		/** Create the TraceAction instance */
		TraceAction(final EventAction anAction) {
			action = anAction;
		}

		/** handle the event */
		@Override
		public boolean handle(final Event evnt, final Object src,
				final EventContext context, final EventParams params) {
			final EventParams traceParam = new EventParams()
					.put("id", ++eventCallId).put("event", evnt.getName())//$NON-NLS-1$ //$NON-NLS-2$
					.put("source", src).put("params", params); //$NON-NLS-1$//$NON-NLS-2$
			// Fire BeforeEventHandling
			if (traceAll) {
				EventEventTrace.BeforeEventHandling.event().fire(Event.class,
						traceParam);
			}
			if (traceMe) {
				EventEventTrace.BeforeEventHandling.event().fire(evnt,
						traceParam);
			}
			// Call the true action
			final boolean retValue = action.handle(evnt, src, context, params);
			// Add return value to message
			// param is store again as it could have been modified
			traceParam.put("returnValue", retValue).put("params", params); //$NON-NLS-1$//$NON-NLS-2$
			// Fire EventHandled
			if (traceAll) {
				EventEventTrace.EventHandled.event().fire(Event.class,
						traceParam);
			}
			if (traceMe) {
				EventEventTrace.EventHandled.event().fire(evnt, traceParam);
			}
			// Well done
			return retValue;
		}
	}
}
